#include <slothttpd/generic_pool.hpp>
#include <slothttpd/logger.hpp>

#include <cstdlib>

#include <algorithm>
#include <stdexcept>

#include <boost/throw_exception.hpp>

namespace slothttpd {

const std::size_t generic_pool::alignment = sizeof(void*);

generic_pool::pool_header::pool_header(std::size_t size) noexcept :
    size_(std::max(size, sizeof(pool_header) + sizeof(generic_pool::large_block_type))),
    last_(begin()),
    next_ (nullptr)
{
    if (size_ != size) {
        logger() << "pool_size is too small (" << size << "), should be at least " 
                 << (sizeof(pool_header) + sizeof(generic_pool::large_block_type)) << '\n';
    }
}

generic_pool::pool_header *generic_pool::pool_header::create_block(std::size_t size)
{
    size = std::max(size, sizeof(pool_header));
    
    pool_header *p = reinterpret_cast<pool_header*>(std::malloc(size));
    
    if (p == nullptr)
        throw std::bad_alloc();
    
    new (p) pool_header(size);
    
    return p;
}

void *generic_pool::pool_header::allocate(std::size_t size) noexcept
{
    char *aligned_last = align_ptr(last_, alignment);
    
    if (std::size_t(end() - aligned_last) >= size) {
        last_ = aligned_last + size;
        return aligned_last;
    }
    
    return nullptr;
}

void generic_pool::pool_header::reset() noexcept
{
    last_ = begin();
}

void generic_pool::pool_header::destroy() noexcept
{
    std::free(reinterpret_cast<void*>(this));
}

char *generic_pool::pool_header::begin() noexcept
{
    return reinterpret_cast<char*>(this) + sizeof(pool_header);
}

char *generic_pool::pool_header::end() noexcept
{
    return reinterpret_cast<char*>(this) + size_;
}

void generic_pool::cleanup_type::clean() noexcept
{
    if (handler != nullptr) {
        handler(arg);
        handler = nullptr;
    }
}

char *generic_pool::align_ptr(char *p, std::size_t a)
{
    return reinterpret_cast<char*>((std::intptr_t(p) + std::intptr_t(a) - 1) & ~(std::intptr_t(a) - 1));
}

generic_pool::generic_pool(std::size_t size) :
    block_size_(size),
    max_alloc_(size < sizeof(pool_header) ? 0 : (size - sizeof(pool_header))),
    first_(nullptr),
    current_(nullptr)
{
}

void generic_pool::reset() noexcept
{
    for (cleanup_type &cleanup : cleanups_) {
        cleanup.clean();
    }
    cleanups_.clear();
    
    for (large_block_type &large : large_blocks_) {
        std::free(large.data);
    }
    large_blocks_.clear();
    
    for (pool_header *p = first_; p != nullptr; p = p->next_) {
        p->reset();
    }
    
    current_ = first_;
}

void generic_pool::free_pool() noexcept
{
    reset();
    
    if (first_ != nullptr) {
        pool_header *p = first_;
        pool_header *n = first_->next_;
        
        while (true) {
            p->destroy();
            
            if (n == nullptr)
                break;
            p = n;
            n = n->next_;
        }
    }
    
    first_ = nullptr;
    current_ = nullptr;
}

generic_pool::~generic_pool() noexcept
{
    free_pool();
}

void *generic_pool::allocate(std::size_t size)
{
    if (first_ == nullptr) {
        first_   = pool_header::create_block(block_size_);
        current_ = first_;
    }
    
    if (size < max_alloc_) {
        return allocate_small(size);
    } else {
        return allocate_large(size);
    }
}

void *generic_pool::allocate_aligned(std::size_t size, std::size_t align)
{
    large_block_type *large = static_cast<large_block_type*>(allocate_small(sizeof(large_block_type)));
    
    new (large) large_block_type();
    
    void *p;
    if (::posix_memalign(&p, align, size) != 0) {
        throw std::bad_alloc();
    }
    
    large->data = static_cast<char*>(p);
    
    large_blocks_.push_front(*large);
    return large->data;
}

void generic_pool::free(void *p) noexcept
{
    if (p != nullptr) {
        for (large_block_type &large : large_blocks_) {
            if (large.data == p) {
                std::free(large.data);
                large.data = nullptr;
                break;
            }
        }
    }
}

void *generic_pool::allocate_small(std::size_t size)
{
    //assert(size <= max_alloc_);
    
    void *p = current_->allocate(size);
    
    if (p == nullptr) {
        pool_header *new_block = pool_header::create_block(block_size_);
        
        current_->next_ = new_block;
        current_ = new_block;
        
        return new_block->allocate(size); // will never return nullptr
    } else {
        return p;
    }
}

void *generic_pool::allocate_large(std::size_t size)
{
    large_block_type *large = static_cast<large_block_type*>(allocate_small(sizeof(large_block_type)));
    
    new (large) large_block_type();
    
    large->data = reinterpret_cast<char*>(std::malloc(size));
    
    if (large->data == nullptr)
        throw std::bad_alloc();
    
    large_blocks_.push_front(*large);
    return large->data;
}

void generic_pool::post_cleanup(void (*handler)(void*), void *arg)
{
    cleanup_type *cleanup = static_cast<cleanup_type*>(allocate(sizeof(cleanup_type)));
    
    new (cleanup) cleanup_type();
    
    cleanup->handler = handler;
    cleanup->arg     = arg;
    cleanups_.push_front(*cleanup);
}

} //namespace slothttpd
